#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#

from abc import ABC, abstractmethod
from typing import ClassVar, Optional

from unicorn import Uc
from unicorn.unicorn import UcContext
from capstone import Cs
from keystone import Ks

from qiling import Qiling
from qiling.const import QL_ARCH, QL_ENDIAN

from .models import QL_CPU
from .register import QlRegisterManager
from .utils import QlArchUtils


class QlArch(ABC):
    type: ClassVar[QL_ARCH]
    bits: ClassVar[int]

    def __init__(self, ql: Qiling, *, cputype: Optional[QL_CPU] = None):
        self.ql = ql

        self.cpu = cputype
        self.utils = QlArchUtils(ql)

    @property
    @abstractmethod
    def uc(self) -> Uc:
        """Get unicorn instance bound to arch.
        """

        pass

    @property
    @abstractmethod
    def regs(self) -> QlRegisterManager:
        """Architectural registers.
        """

        pass

    @property
    def pointersize(self) -> int:
        return self.bits // 8

    def stack_push(self, value: int) -> int:
        """Push a value onto the architectural stack.

        Args:
            value: a numeric value to push

        Returns: the top of stack after pushing the value
        """

        self.regs.arch_sp -= self.pointersize
        self.ql.mem.write_ptr(self.regs.arch_sp, value)

        return self.regs.arch_sp

    def stack_pop(self) -> int:
        """Pop a value from the architectural stack.

        Returns: the value at the top of stack
        """

        data = self.ql.mem.read_ptr(self.regs.arch_sp)
        self.regs.arch_sp += self.pointersize

        return data

    def stack_read(self, offset: int) -> int:
        """Peek the architectural stack at a specified offset from its top, without affecting
        the top of the stack.

        Note that this operation violates the FIFO property of the stack and may be used cautiously.

        Args:
            offset: offset in bytes from the top of the stack, not necessarily aligned to the
                    native stack item size. the offset may be either positive or netagive, where
                    a 0 value means retrieving the value at the top of the stack

        Returns: the value at the specified address
        """

        return self.ql.mem.read_ptr(self.regs.arch_sp + offset)

    def stack_write(self, offset: int, value: int) -> None:
        """Write a value to the architectural stack at a specified offset from its top, without
        affecting the top of the stack.

        Note that this operation violates the FIFO property of the stack and may be used cautiously.

        Args:
            offset: offset in bytes from the top of the stack, not necessarily aligned to the
                    native stack item size. the offset may be either positive or netagive, where
                    a 0 value means overwriting the value at the top of the stack
        """

        self.ql.mem.write_ptr(self.regs.arch_sp + offset, value)

    # Unicorn's CPU state save
    def save(self) -> UcContext:
        return self.uc.context_save()

    # Unicorn's CPU state restore method
    def restore(self, saved_context: UcContext):
        self.uc.context_restore(saved_context)

    @property
    @abstractmethod
    def disassembler(self) -> Cs:
        """Get disassembler instance bound to arch.
        """

        pass

    @property
    @abstractmethod
    def assembler(self) -> Ks:
        """Get assembler instance bound to arch.
        """

        pass

    @property
    @abstractmethod
    def endian(self) -> QL_ENDIAN:
        """Get processor endianness.
        """

        pass
